﻿#region File and License Information
/*
<File>
	<License>
		Copyright © 2009 - 2017, Daniel Vaughan. All rights reserved.
		This file is part of Calcium (http://calciumsdk.net), 
		which is released under the MIT License.
		See file /Documentation/License.txt for details.
	</License>
	<CreationDate>2011-11-17 16:09:31Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;

using Outcoder.ComponentModel.InputValidation;
using Outcoder.ComponentModel.StatePreservation;
using Outcoder.Data.Serialization;
using Outcoder.Logging;
using Outcoder.NavigationModel;
using Outcoder.Net;
using Outcoder.Reflection;
using Outcoder.Services;
using Outcoder.Services.Implementation;
using Outcoder.UI.Xaml;
#if NETFX_CORE
using Windows.UI.Xaml.Navigation;
using Console=System.Diagnostics.Debug;
#else
using System.Windows;
using System.Windows.Navigation;
#endif

namespace Outcoder.ComponentModel
{
	public interface IViewModel
	{
		object Title { get; }

		object Glyph { get; }
	}

	public interface ICompositeViewModel
	{
		IEnumerable<ViewModelBase> ChildViewModels
		{
			get;
		}

		void ActivateViewModel(ViewModelBase viewModel);
	}

	public abstract class ViewModelBase
		: ObservableObject, IViewModel, INotifyDataErrorInfo, 
			IValidateData, IStatePreservation, INavigationAware
	{
		readonly DataErrorNotifier dataErrorNotifier;

		protected DataErrorNotifier DataErrorNotifier => dataErrorNotifier;

		/// <summary>
		/// If <c>true</c> this object will have automatic state persistence applied.
		/// If <c>false</c> no state persistence is performed and state attributes 
		/// are not read. 
		/// The default value is <c>true</c>.
		/// </summary>
		protected bool StatePersistenceEnabled { get; set; } = true;

		/// <summary>
		/// Enables reading of the <seealso cref="StatefulAttribute"/>
		/// for class properties.
		/// Setting this property to <c>false</c> may improve performance
		/// because there is a cost to reading the state attributes for the object.
		/// The default value is <c>true</c>.
		/// </summary>
		protected bool StatefulAttributesEnabled { get; set; } = true;

		protected ViewModelBase() 
			: this((string)null)
		{
			/* Intentionally left blank. */
		}

		protected ViewModelBase(string title) 
			: this((Func<object>)null)
		{
			this.title = title;
		}

		protected ViewModelBase(Func<object> titleFunc) 
		{
			this.titleFunc = titleFunc;
			
			if (EnvironmentValues.DesignTime)
			{
				return;
			}

			dataErrorNotifier = new DataErrorNotifier(this, this);
		}

		void EnsureStateAttributesRead()
		{
			if (!StatePersistenceEnabled)
			{
				return;
			}

			if (viewState == null)
			{
				lock (viewStateCreationLock)
				{
					if (viewState == null)
					{
						viewState = new ViewState();

						if (StatefulAttributesEnabled)
						{
							ReadStateAttributes();
						}
					}
				}
			}
		}

		#region Title
		object title;

		/// <summary>
		/// Gets or sets the title to display in the UI for the view.
		/// If a Func has been supplied to resolve the title the Func is used 
		/// only if the title value is null.
		/// </summary>
		public virtual object Title
		{
			get
			{
				if (title != null)
				{
					return title;
				}

				if (titleFunc != null)
				{
					var result = titleFunc();
					return result;
				}

				return null;
			}
			set
			{
				Assign(ref title, value);
			}
		}

		Func<object> titleFunc;

		/// <summary>
		/// A Func to retrieve the title of the view.
		/// <seealso cref="Title"/>
		/// </summary>
		public Func<object> TitleFunc
		{
			get
			{
				return titleFunc;
			}
			set
			{
				if (Assign(ref titleFunc, value) == AssignmentResult.Success)
				{
					OnPropertyChanged("Title");
				}
			}
		}
		#endregion

		#region Glyph
		object glyph = defaultGlyph;
		static object defaultGlyph = '\uE922';

		public static object DefaultGlyph
		{
			get
			{
				return defaultGlyph;
			}
			set
			{
				defaultGlyph = value;
			}
		}

		/// <summary>
		/// Gets or sets the glyph to display in the UI for the view.
		/// If a Func has been supplied to resolve the glyph the Func is used 
		/// only if the glyph value is null.
		/// </summary>
		public virtual object Glyph
		{
			get
			{
				if (glyph != null && glyph != defaultGlyph)
				{
					return glyph;
				}

				if (glyphFunc != null)
				{
					var result = glyphFunc();
					return result;
				}

				return defaultGlyph;
			}
			set
			{
				Assign(ref glyph, value);
			}
		}

		Func<object> glyphFunc;

		/// <summary>
		/// A Func to retrieve the glyph of the view.
		/// <seealso cref="Glyph"/>
		/// </summary>
		public Func<object> GlyphFunc
		{
			get
			{
				return glyphFunc;
			}
			set
			{
				if (Assign(ref glyphFunc, value) == AssignmentResult.Success)
				{
					OnPropertyChanged("Glyph");
				}
			}
		}
		#endregion

		bool busy;

		/// <summary>
		/// Indicates when the viewmodel is busy processing, 
		/// and a progress indicator should be displayed.
		/// </summary>
		public bool Busy
		{
			get
			{
				return busy;
			}
			protected set
			{
				Assign(ref busy, value);
			}
		}

		string busyMessage;

		/// <summary>
		/// The message displayed when the viewmodel is busy.
		/// </summary>
		public string BusyMessage
		{
			get
			{
				return busyMessage;
			}
			set
			{
				Assign(ref busyMessage, value);
			}
		}

		protected IDialogService DialogService => Dependency.Resolve<IDialogService, DialogService>();

		public IMarketplaceService MarketplaceService
		{
			get
			{
#if !WINDOWS_UWP && !NETFX_CORE && !WINDOWS_PHONE
				var marketplaceService = Dependency.Resolve<IMarketplaceService>();
#else
				var marketplaceService = Dependency.Resolve<IMarketplaceService, MarketplaceService>();
#endif
				return marketplaceService;
			}
		}

		protected ILog Log => Dependency.Resolve<ILog>();

		#region Input Validation
		/// <summary>
		/// Gets the property errors. This should be overriden in a subclass
		/// to provide property validation.
		/// </summary>
		/// <param name="propertyName">Name of the property to validate.</param>
		/// <param name="value">The proposed value of a property.</param>
		/// <returns>A list of validation errors; 
		/// an empty list if no errors are found.</returns>
		protected virtual IEnumerable<DataValidationError> GetPropertyErrors(
			string propertyName, object value)
		{
			return new List<DataValidationError>();
		}

		/// <summary>
		/// Adds the property to the list of known class properties, 
		/// which is used, for example, when performing validation 
		/// of the whole class instance.
		/// </summary>
		/// <param name="name">The name of the property.</param>
		/// <param name="propertyFunc">The <c>Func</c> to 
		/// retrieve the property.</param>
		protected void AddValidationProperty(
			string name, Func<object> propertyFunc)
		{
			dataErrorNotifier.AddValidationProperty(name, propertyFunc);
		}

		protected void AddValidationProperty(Expression<Func<object>> expression)
		{
			PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;

#if NETFX_CORE
			MethodInfo methodInfo = propertyInfo.GetMethod;
			Func<object> getter = (Func<object>)methodInfo.CreateDelegate(
													typeof(Func<object>),
													this);
#else
			Func<object> getter = (Func<object>)Delegate.CreateDelegate(
										typeof(Func<object>),
										this,
										propertyInfo.GetGetMethod());
#endif
			dataErrorNotifier.AddValidationProperty(name, getter);
		}

		/// <summary>
		/// Determines whether this viewmodel contains property errors. 
		/// This is done by checking it for data validation errors.
		/// </summary>
		public async Task ValidateAllAsync(bool raisePropertyChanged = true)
		{
			await dataErrorNotifier.ValidateAllAsync();

			if (raisePropertyChanged)
			{
				/* This causes any Framework elements to update 
				 * and show or hide error messages as required. */
				OnPropertyChanged(string.Empty);
			}
		}

		public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
		{
			add
			{
				dataErrorNotifier.ErrorsChanged += value;
			}
			remove
			{
				dataErrorNotifier.ErrorsChanged -= value;
			}
		}

		public IEnumerable GetErrors(string propertyName)
		{
			return dataErrorNotifier.GetErrors(propertyName);
		}

		public bool HasErrors => dataErrorNotifier.HasErrors;

		public ReadOnlyDictionary<string, ObservableCollection<DataValidationError>> ValidationErrors
			=> dataErrorNotifier.ValidationErrors;

		public virtual Task<ValidationCompleteEventArgs> ValidateAsync(string memberName, object value)
		{
			IEnumerable<DataValidationError> errors;
			try
			{
				errors = GetPropertyErrors(memberName, value);
			}
			catch (Exception ex)
			{
				return Task.FromResult(new ValidationCompleteEventArgs(memberName, ex));
			}
			
			return Task.FromResult(new ValidationCompleteEventArgs(memberName, errors));
		}

#endregion

#region State Management

		protected byte[] Serialize(object value)
		{
			byte[] state = SilverlightSerializer.Serialize(value);
			return state;
		}

		protected T Deserialize<T>(byte[] data) where T : class
		{
			T result = SilverlightSerializer.Deserialize<T>(data);
			return result;
		}

		protected void RegisterStatefulProperty<TProperty>(
			ApplicationStateType applicationStateType,
			Expression<Func<TProperty>> expression,
			Action<TProperty> setAction = null)
		{
			if (!StatePersistenceEnabled)
			{
				throw new InvalidOperationException("StatePersistenceEnabled must be set to true to register a stateful property.");
			}

			EnsureStateAttributesRead();

			RegisterStatefulProperty((name, getter, setter)
				=> viewState.RegisterState(
							name, getter, setter, applicationStateType),
						expression, setAction);
		}

		protected void DeregisterStatefulProperty(
			ApplicationStateType? applicationStateType,
			Expression<Func<object>> expression)
		{
			if (!StatePersistenceEnabled)
			{
				throw new InvalidOperationException("StatePersistenceEnabled must be set to true to de-register a stateful property.");
			}

			EnsureStateAttributesRead();

			PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;
			viewState.DeregisterState(name, applicationStateType);
		}

		protected void RegisterState<T>(
			string stateKey,
			Func<T> getFunc,
			Action<T> setAction,
			ApplicationStateType applicationStateType)
		{
			if (!StatePersistenceEnabled)
			{
				throw new InvalidOperationException("StatePersistenceEnabled must be set to true to register a stateful property.");
			}

			EnsureStateAttributesRead();

			viewState.RegisterState(
				stateKey, getFunc, setAction, applicationStateType);
		}

		protected void DeregisterState<T>(
			string stateKey, ApplicationStateType? applicationStateType = null)
		{
			if (!StatePersistenceEnabled)
			{
				throw new InvalidOperationException(
					"StatePersistenceEnabled must be set to true to de-register a stateful property.");
			}

			EnsureStateAttributesRead();

			viewState.DeregisterState(stateKey, applicationStateType);
		}

		void RegisterStatefulProperty<T>(
			Action<string, Func<T>, Action<T>> registerAction,
			Expression<Func<T>> expression, Action<T> setAction = null)
		{
			ArgumentValidator.AssertNotNull(registerAction, "registerAction");
			ArgumentValidator.AssertNotNull(expression, "expression");

			PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
			string name = propertyInfo.Name;
			var propertyGetterFunc = propertyInfo.CreateGetter<T>(this);

			if (setAction == null)
			{
				try
				{
					setAction = propertyInfo.CreateSetter<T>(this);
				}
				catch (Exception ex)
				{
					string message = string.Format(
						"Unable to get setter for property '{0}' {1} ", name, ex);
					Console.WriteLine(message);
					Debug.Assert(false, message);
					return;
				}
			}
			registerAction(name, propertyGetterFunc, setAction);
		}

		ViewState viewState;
		readonly object viewStateCreationLock = new object();

		public virtual void LoadState(
			IDictionary<string, object> persistentStateDictionary,
			IDictionary<string, object> transientStateDictionary,
			bool shouldLoadTransientState)
		{
			EnsureStateAttributesRead();

			viewState?.LoadPersistentState(persistentStateDictionary);
			if (shouldLoadTransientState)
			{
				viewState?.LoadTransientState(transientStateDictionary);
			}
		}

		public virtual void SaveState(
			IDictionary<string, object> persistentStateDictionary,
			IDictionary<string, object> transientStateDictionary)
		{
			EnsureStateAttributesRead();

			viewState?.SavePersistentState(persistentStateDictionary);
			viewState?.SaveTransientState(transientStateDictionary);
		}

		void ReadStateAttributes()
		{
#if NETFX_CORE
			var properties = GetType().GetTypeInfo().DeclaredProperties;
#else
			var properties = GetType().GetProperties();
#endif
			foreach (PropertyInfo propertyInfo in properties)
			{
				var attributes = propertyInfo.GetCustomAttributes(
											typeof(StatefulAttribute), true).ToList();

				if (!attributes.Any())
				{
					continue;
				}

				StatefulAttribute attribute
					= (StatefulAttribute)attributes[0];
				var persistenceType = attribute.StateType;

				if (!propertyInfo.CanRead || !propertyInfo.CanWrite)
				{
					throw new InvalidOperationException(string.Format(
						"Property {0} must have a getter and a setter to be persisted.",
						propertyInfo.Name));
				}

				/* Prevents access to internal closure warning. */
				PropertyInfo info = propertyInfo;

				viewState.RegisterState(
					propertyInfo.Name,
					() => info.GetValue(this, null),
					obj => info.SetValue(this, obj, null),
					persistenceType);
			}
		}

#endregion

#region Navigation 
		/* (see INavigationService and its FrameNavigationService implementation, and INavigationAware) */

		INavigationService GetNavigationService()
		{
#if NETFX_CORE || WINDOWS_UWP || WINDOWS_PHONE
			var navigationService = Dependency.Resolve<INavigationService, FrameNavigationService>();
#else
			var navigationService = Dependency.Resolve<INavigationService>();
#endif
			return navigationService;
		}

		/// <summary>
		/// Navigates to the previous page.
		/// </summary>
		protected void GoBack()
		{
			var navigationService = GetNavigationService();
			navigationService.GoBack();
		}

#if WINDOWS_UWP || NETFX_CORE
		/// <summary>
		/// Navigates to the specified page type.
		/// </summary>
		/// <param name="pageType">The source page type that will be instanciated and displayed.</param>
		/// <param name="parameters">Parameters that the page is expecting.</param>
		protected void Navigate(Type pageType, object parameters = null)
		{
			var navigationService = GetNavigationService();
			navigationService.Navigate(pageType, parameters);
		}
#else
		/// <summary>
		/// Navigates to the specified URI.
		/// </summary>
		/// <param name="source">The source URI.</param>
		protected void Navigate(Uri source)
		{
			var navigationService = GetNavigationService();
			navigationService.Navigate(source);
		}

		/// <summary>
		/// Navigates to the specified relative URL.
		/// The URL should be relative to the root of the application 
		/// and begin with a slash e.g., "/Views/Settings.xaml".
		/// </summary>
		/// <param name="relativeUrl">The relative URL.</param>
		protected void Navigate(string relativeUrl)
		{
			INavigationService navigationService = GetNavigationService();
			navigationService.Navigate(relativeUrl);
		}
#endif

		protected event EventHandler<NavigationArgs> NavigatedTo;

		protected void OnNavigatedTo(NavigationArgs e)
		{
			NavigatedTo?.Invoke(this, e);
		}

		IDictionary<string, string> queryParameters = new Dictionary<string, string>();
		
		protected IDictionary<string, string> QueryParameters => queryParameters;

		void INavigationAware.HandleNavigatedTo(NavigationArgs e)
		{
#if !(NETFX_CORE || WINDOWS_UWP)
			if (e.Uri != null)
			{
				string decodedUrl = HttpUtilityExtended.UrlDecode(e.Uri.ToString());

				queryParameters = HttpUtilityExtended.ParseQueryString(decodedUrl);
			}
#endif
			OnNavigatedTo(e);
		}

		protected event EventHandler<NavigatingFromEventArgs> NavigatingFrom;

		protected void OnNavigatingFrom(NavigatingFromEventArgs e)
		{
			NavigatingFrom?.Invoke(this, e);
		}

		void INavigationAware.HandleNavigatingFrom(NavigatingCancelArgs e)
		{
#if NETFX_CORE || WINDOWS_UWP
			var navigationService = Dependency.Resolve<INavigationService, FrameNavigationService>();

			if (e.SourcePageType == navigationService.SourcePageType)
			{
				return;
			}

			NavigatingFromEventArgs args = new NavigatingFromEventArgs(e.NavigationMode, e.SourcePageType);
#elif WINDOWS_PHONE
			var navigationService = Dependency.Resolve<INavigationService, FrameNavigationService>();

			if (e.Uri.Equals(navigationService.Source))
			{
				return;
			}

			NavigatingFromEventArgs args = new NavigatingFromEventArgs(e.NavigationMode, e.Uri);
#else
			var navigationService = Dependency.Resolve<INavigationService>();

			if (e.Uri.Equals(navigationService.Source))
			{
				return;
			}

			NavigatingFromEventArgs args = new NavigatingFromEventArgs(e.NavigationMode, e.Uri);
#endif

			OnNavigatingFrom(args);
			if (args.Cancel)
			{
				e.Cancel = true;
			}
		}

#endregion

		/// <summary>
		/// Invokes the specified action on the UI thread. 
		/// If this call is made from the UI thread, 
		/// the action is performed synchronously.
		/// <seealso cref="DispatcherExtensions.InvokeIfRequired" />
		/// </summary>
		/// <param name="action"></param>
		protected void InvokeIfRequired(Action action)
		{
			UISynchronizationContext.Instance.InvokeIfRequired(action);
		}

		/// <summary>
		/// Executes the specified delegate without blocking. 
		/// </summary>
		/// <param name="action"></param>
		protected void BeginInvoke(Action action)
		{
			UISynchronizationContext.Instance.InvokeWithoutBlocking(action);
		}
	}

	public class NavigatingFromEventArgs : CancelEventArgs
	{
		public NavigationMode NavigationMode { get; private set; }

#if NETFX_CORE
		public Type SourceType { get; private set; }

		public NavigatingFromEventArgs(NavigationMode navigationMode, Type sourceType)
		{
			NavigationMode = navigationMode;
			SourceType = sourceType;
		}
#else
		public Uri Uri { get; private set; }

		public NavigatingFromEventArgs(NavigationMode navigationMode, Uri uri)
		{
			NavigationMode = navigationMode;
			Uri = uri;
		}
#endif
		
	}
}
